#include "Nvram.hpp"
#include "Notification.hpp"
#include "Util.hpp"
#include <cpp3ds/System/I18n.hpp>
#include <cstring>

// https://github.com/devkitPro/libnds/blob/master/source/arm7/userSettings.c

namespace AdvDSProfile
{

NVRAM &NVRAM::getInstance()
{
  static NVRAM nvram;
  return nvram;
}

Result NVRAM::nvramInit()
{
  Result res;

  if (R_FAILED(res = cfgnorInit(1))) {
  	Notification::spawn(_("Unable to start CFGNOR: \n0x%08lX (%d)", res, R_DESCRIPTION(res)));
  	return res;
  }

  if (R_FAILED(res = CFGNOR_Initialize(1))) {
  	Notification::spawn(_("Unable to init CFGNOR: \n0x%08lX (%d)", res, R_DESCRIPTION(res)));
  	return res;
  }

  return 0;
}

Result NVRAM::nvramExit()
{
  Result res;

  if (R_FAILED(res = CFGNOR_Shutdown())) {
		Notification::spawn(_("Unable to shutdown CFGNOR: \n0x%08lX (%d)", res, R_DESCRIPTION(res)));
    return res;
  }

	cfgnorExit();

  return 0;
}

Result NVRAM::readUsername(cpp3ds::String *usernameOut)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 usernameAddress = getNVRAMActiveBlock() + 0x6;

  u16 *username = (u16*)malloc(0x14 * sizeof(u16));
	res = CFGNOR_ReadData(usernameAddress, (u32*)username, 0x14);

  cpp3ds::String returned = cpp3ds::String::fromUtf16(username, username + 0x14);
  *usernameOut = returned.substring(0, 10);

  nvramExit();

  return res;
}

Result NVRAM::writeUsername(cpp3ds::String usernameIn)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 activeBlockAddress = getNVRAMActiveBlock();

  cpp3ds::Uint32 usernameAddress = activeBlockAddress + 0x6;
  cpp3ds::Uint32 usernameLenAddress = activeBlockAddress + 0x1A;

  u16 *strLen = (u16*)malloc(0x2 * sizeof(u16));
  *strLen = usernameIn.getSize();

  res = CFGNOR_WriteData(usernameAddress, (u32*)usernameIn.toUtf16().c_str(), 0x14);
  printf("res#1: 0x%08lX\n", res);
  res = CFGNOR_WriteData(usernameLenAddress, (u32*)strLen, 0x2);
  printf("res#2: 0x%08lX\n", res);

  writeCRC(activeBlockAddress);

  getNVRAMActiveBlock();

  nvramExit();

  return res;
}

Result NVRAM::readMessage(cpp3ds::String *messageOut)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 messageAddress = getNVRAMActiveBlock() + 0x1C;

  u16 *message = (u16*)malloc(0x34 * sizeof(u16));
	res = CFGNOR_ReadData(messageAddress, (u32*)message, 0x34);

  cpp3ds::String returned = cpp3ds::String::fromUtf16(message, message + 0x34);
  *messageOut = returned.substring(0, 26);

  nvramExit();

  return res;
}

Result NVRAM::writeMessage(cpp3ds::String messageIn)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 activeBlockAddress = getNVRAMActiveBlock();

  cpp3ds::Uint32 messageAddress = activeBlockAddress + 0x1C;
  cpp3ds::Uint32 messageLenAddress = activeBlockAddress + 0x50;

  u16 *strLen = (u16*)malloc(0x2 * sizeof(u16));
  *strLen = messageIn.getSize();

  res = CFGNOR_WriteData(messageAddress, (u32*)messageIn.toUtf16().c_str(), 0x34);
  printf("res#1: 0x%08lX\n", res);
  res = CFGNOR_WriteData(messageLenAddress, (u32*)strLen, 0x2);
  printf("res#2: 0x%08lX\n", res);

  writeCRC(activeBlockAddress);

  getNVRAMActiveBlock();

  nvramExit();

  return res;
}

Result NVRAM::readBirthday(int *birthdayDayOut, int *birthdayMonthOut)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 activeBlockAddress = getNVRAMActiveBlock();

  cpp3ds::Uint32 bdayDayAddress = activeBlockAddress + 0x4;
  cpp3ds::Uint32 bdayMthAddress = activeBlockAddress + 0x3;

  u8 *bdayDay = (u8*)malloc(0x2 * sizeof(u8));
	res = CFGNOR_ReadData(bdayDayAddress, (u32*)bdayDay, 0x2);
  u8 *bdayMth = (u8*)malloc(0x2 * sizeof(u8));
	res = CFGNOR_ReadData(bdayMthAddress, (u32*)bdayMth, 0x2);

  *birthdayDayOut = *bdayDay;
  *birthdayMonthOut = *bdayMth;

  nvramExit();

  return res;
}

Result NVRAM::writeBirthdayMonth(int birthdayMonthIn)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 activeBlockAddress = getNVRAMActiveBlock();

  cpp3ds::Uint32 bdayMthAddress = activeBlockAddress + 0x3;

  u8 *bdayMth = (u8*)malloc(0x1 * sizeof(u8));
  *bdayMth = birthdayMonthIn;

  res = CFGNOR_WriteData(bdayMthAddress, (u32*)bdayMth, 0x1);
  printf("res#1: 0x%08lX\n", res);

  writeCRC(activeBlockAddress);

  getNVRAMActiveBlock();

  nvramExit();

  return res;
}

Result NVRAM::writeBirthdayDay(int birthdayDayIn)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 activeBlockAddress = getNVRAMActiveBlock();

  cpp3ds::Uint32 bdayDayAddress = activeBlockAddress + 0x4;

  u8 *bdayDay = (u8*)malloc(0x1 * sizeof(u8));
  *bdayDay = birthdayDayIn;

  res = CFGNOR_WriteData(bdayDayAddress, (u32*)bdayDay, 0x1);
  printf("res#1: 0x%08lX\n", res);

  writeCRC(activeBlockAddress);

  getNVRAMActiveBlock();

  nvramExit();

  return res;
}

Result NVRAM::readTheme(cpp3ds::Color *colorOut, int *indexOut)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 activeBlockAddress = getNVRAMActiveBlock();
  cpp3ds::Color themeColors[] = {cpp3ds::Color(97, 130, 154), cpp3ds::Color(186, 73, 0), cpp3ds::Color(251, 0, 24), cpp3ds::Color(251, 138, 251), cpp3ds::Color(251, 146, 0), cpp3ds::Color(243, 227, 0), cpp3ds::Color(170, 251, 0), cpp3ds::Color(0, 251, 0), cpp3ds::Color(0, 162, 56), cpp3ds::Color(73, 219, 138), cpp3ds::Color(48, 186, 243), cpp3ds::Color(0, 89, 243), cpp3ds::Color(0, 0, 146), cpp3ds::Color(138, 0, 211), cpp3ds::Color(211, 0, 235), cpp3ds::Color(251, 0, 146)};

  cpp3ds::Uint32 themeAddress = activeBlockAddress + 0x2;

  u8 *theme = (u8*)malloc(0x2 * sizeof(u8));
	res = CFGNOR_ReadData(themeAddress, (u32*)theme, 0x2);

  *colorOut = themeColors[*theme];
  *indexOut = *theme;

  nvramExit();

  return res;
}

Result NVRAM::writeTheme(int themeIn)
{
  Result res;

  if (R_FAILED(res = nvramInit()))
    return res;

  cpp3ds::Uint32 activeBlockAddress = getNVRAMActiveBlock();

  cpp3ds::Uint32 themeAddress = activeBlockAddress + 0x2;

  u8 *theme = (u8*)malloc(0x1 * sizeof(u8));
  *theme = themeIn;

  res = CFGNOR_WriteData(themeAddress, (u32*)theme, 0x1);
  printf("res#1: 0x%08lX\n", res);

  writeCRC(activeBlockAddress);

  getNVRAMActiveBlock();

  nvramExit();

  return res;
}

cpp3ds::Uint32 NVRAM::getNVRAMActiveBlock()
{
	Result res;

	u16 *userSettingsBase = (u16*)malloc(0x2 * sizeof(u16));

	res = CFGNOR_ReadData(0x20, (u32*)userSettingsBase, 0x2);

	//printf("userSettingsBase: 0x%08lX\n", *userSettingsBase);

	cpp3ds::Uint32 slot1Address = *userSettingsBase * 8;
	cpp3ds::Uint32 slot2Address = *userSettingsBase * 8 + 0x100;

	//printf("addrSlot1: 0x%08lX, addrSlot2: 0x%08lX\n", slot1Address, slot2Address);

	u16 *slot1Count = (u16*)malloc(0x2 * sizeof(u16));
	u16 *slot2Count = (u16*)malloc(0x2 * sizeof(u16));
	u16 *slot1CRC = (u16*)malloc(0x2 * sizeof(u16));
	u16 *slot2CRC = (u16*)malloc(0x2 * sizeof(u16));

	res = CFGNOR_ReadData(slot1Address + 0x70, (u32*)slot1Count, 0x2);
	res = CFGNOR_ReadData(slot2Address + 0x70, (u32*)slot2Count, 0x2);
	res = CFGNOR_ReadData(slot1Address + 0x72, (u32*)slot1CRC, 0x2);
	res = CFGNOR_ReadData(slot2Address + 0x72, (u32*)slot2CRC, 0x2);

	//printf("Slot 1: cnt 0x%08lX crc 0x%08lX\nSlot 2: cnt 0x%08lX crc 0x%08lX\n", *slot1Count, *slot1CRC, *slot2Count, *slot2CRC);

	u16 *slot1Data = (u16*)malloc(0x70 * sizeof(u16));
	u16 *slot2Data = (u16*)malloc(0x70 * sizeof(u16));

	res = CFGNOR_ReadData(slot1Address, (u32*)slot1Data, 0x70);
	res = CFGNOR_ReadData(slot2Address, (u32*)slot2Data, 0x70);

	cpp3ds::Uint16 slot1ComputedCRC = ModRTU_CRC(slot1Data, 0x70);
	cpp3ds::Uint16 slot2ComputedCRC = ModRTU_CRC(slot2Data, 0x70);

	//printf("CRC data1: 0x%08lX\nCRC data2: 0x%08lX\n", slot1ComputedCRC, slot2ComputedCRC);

	if (*slot1CRC != slot1ComputedCRC && *slot2CRC != slot2ComputedCRC) {
		//printf("Oooops, CRC doesn't match\n");
		return 0x0;
	}

	cpp3ds::Uint32 activeAddress = slot1Address;

	if (*slot1CRC == slot1ComputedCRC && *slot2CRC == slot2ComputedCRC) {
		printf("Both CRC matches !\n");
		activeAddress = (*slot2Count == ((*slot1Count + 1) & 0x7f) ? slot2Address : slot1Address);
	} else {
		if (*slot2CRC == slot2ComputedCRC)
			activeAddress = slot2Address;
	}

	//printf("Chosen address: 0x%08lX\n", activeAddress);

	return activeAddress;
}

Result NVRAM::writeCRC(cpp3ds::Uint32 activeBlockAddress)
{
  Result res;

  u16 *activeSlotData = (u16*)malloc(0x70 * sizeof(u16));
	res = CFGNOR_ReadData(activeBlockAddress, (u32*)activeSlotData, 0x70);
	u16 *activeSlotComputedCRC = (u16*)malloc(0x2 * sizeof(u16));
  *activeSlotComputedCRC = ModRTU_CRC(activeSlotData, 0x70);

  res = CFGNOR_WriteData(activeBlockAddress + 0x72, (u32*)activeSlotComputedCRC, 0x2);
  printf("CRC activeslot: 0x%08lX @ 0x%08lX\nres: 0x%08lX\n", *activeSlotComputedCRC, activeBlockAddress + 0x72, res);
}

} // namespace AdvDSProfile
